package lumis.portal.presentation.core;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Iterator;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import lumis.portal.PortalConfiguration;
import lumis.portal.PortalContext;
import lumis.portal.PortalException;
import lumis.portal.manager.ManagerFactory;
import lumis.portal.page.PageWebResource;
import lumis.portal.presentation.perspective.IPortalMode;
import lumis.portal.presentation.perspective.Perspective;
import lumis.portal.presentation.tag.el.ELMarkupFilter;
import lumis.util.PortalUtil;
import lumis.util.log.ILogger;
import lumis.util.log.LoggerFactory;

import org.apache.wicket.Component;
import org.apache.wicket.IPageFactory;
import org.apache.wicket.IRequestTarget;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.Page;
import org.apache.wicket.PageParameters;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.application.DefaultClassResolver;
import org.apache.wicket.application.IClassResolver;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.IMarkupParserFactory;
import org.apache.wicket.markup.MarkupException;
import org.apache.wicket.markup.MarkupNotFoundException;
import org.apache.wicket.markup.MarkupParser;
import org.apache.wicket.markup.MarkupParserFactory;
import org.apache.wicket.markup.MarkupResourceStream;
import org.apache.wicket.markup.MarkupStream;
import org.apache.wicket.markup.parser.IMarkupFilter;
import org.apache.wicket.markup.parser.filter.RelativePathPrefixHandler;
import org.apache.wicket.markup.resolver.IComponentResolver;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.protocol.http.WebRequestCycle;
import org.apache.wicket.protocol.http.WebRequestCycleProcessor;
import org.apache.wicket.request.IRequestCycleProcessor;
import org.apache.wicket.session.ISessionStore;
import org.apache.wicket.util.time.Duration;

/**
 * The Wicket application implementation used for processing the layout file.
 *
 * @version $Revision: 13093 $ $Date: 2011-05-28 18:40:18 -0300 (Sat, 28 May 2011) $
 * @since 6.0.0
 */
public class LayoutFileApplication extends WebApplication 
{
	private static final ILogger logger = LoggerFactory.getLogger(LayoutFileApplication.class);
	
	/**
	 * Custom {@link WebRequestCycleProcessor} implementation for the
	 * {@link LayoutFileApplication}.
	 * 
	 *
	 * @version $Revision: 13093 $ $Date: 2011-05-28 18:40:18 -0300 (Sat, 28 May 2011) $
	 * @since 6.0.0
	 */
	private static class LayoutFileWebRequestCycleProcessor extends WebRequestCycleProcessor
	{
		/**
		 * Request target that does a server-side dispatch to the
		 * <code>defaultErrorPage.jsp</code>.
		 * 
		 * @version $Revision: 13093 $ $Date: 2011-05-28 18:40:18 -0300 (Sat, 28 May 2011) $
		 * @since 6.0.0
		 */
		private static class DefaultErrorPageRequestTarget implements IRequestTarget
		{
			private final Throwable throwable;
			
			public DefaultErrorPageRequestTarget(Throwable throwable)
			{
				this.throwable = throwable;
			}

			public void respond(RequestCycle requestCycle)
			{
				WebRequestCycle webRequestCycle = (WebRequestCycle)requestCycle;
				HttpServletRequest httpServletRequest = webRequestCycle.getWebRequest().getHttpServletRequest();
				HttpServletResponse httpServletResponse = webRequestCycle.getWebResponse().getHttpServletResponse();
				try
				{
					httpServletRequest.setAttribute("exception", throwable);
					httpServletRequest.getRequestDispatcher("/lumis/portal/error/defaultErrorPage.jsp").forward(httpServletRequest, httpServletResponse);
				}
				catch (Exception e)
				{
					logger.error("Exception thrown while dispatching to customized error page", e);
					throw new RuntimeException(e);
				}
			}

			public void detach(RequestCycle requestCycle)
			{
			}
		}

		@Override
		public void respond(RuntimeException exception, RequestCycle requestCycle)
		{
			Page responsePage = requestCycle.getResponsePage();
			if (!(responsePage instanceof LayoutFilePage))
			{
				// restart wicket processing with DefaultErrorPageRequestTarget
				throw new RestartResponseWithTargetException(new DefaultErrorPageRequestTarget(exception));
			}
				
			PageWebResource pageWebResource = ((LayoutFilePage)responsePage).getPageWebResource();
			if (exception instanceof MarkupException)
			{
				MarkupStream markupStream = ((MarkupException)exception).getMarkupStream();
				if (markupStream != null)
				{
					// MarkupException usually has a huge message since all markup is in it.
					// Create one with a more nice message, but leaving
					// toString and stackTrace with the full message
					final MarkupException originalMarkupException = (MarkupException)exception;
					String message = "Error while processing markup '" + 
							originalMarkupException.getMarkupStream().getResource() + "' for web resource '" + 
							pageWebResource + "'";
					if (exception.getCause() != null)
						message += ": " + originalMarkupException.getCause().getMessage();
					exception = new LayoutFileMarkupException(message, originalMarkupException);
				}
			}
			else if (exception instanceof MarkupNotFoundException)
			{
				final MarkupNotFoundException originalMarkupException = (MarkupNotFoundException)exception;
				
				String message = "The layout file defined for web resource '" + pageWebResource.getName() +
						"' could not be found at '" + PortalContext.getDefinitionPath(pageWebResource.getPage().getLayoutFilePath()) + "'";
				
				exception = new RuntimeException(message, originalMarkupException);
			}

			// log error
			logger.error("Exception thrown during request for '" + pageWebResource + "'", exception);
			
			// check if request is in admin mode
			boolean inAdminMode;
			try
			{
				String cookieMode = PortalUtil.getCookie("lumMode", ((WebRequestCycle)requestCycle).getWebRequest().getHttpServletRequest());
				inAdminMode = cookieMode != null && Integer.parseInt(cookieMode) == Perspective.ADMIN.value();
			}
			catch (PortalException e)
			{
				inAdminMode = false;
			}
				
			if (inAdminMode)
			{
				// show detailed error page
				LayoutFileExceptionErrorPage.setExceptionInfo(requestCycle.getSession(),
						new LayoutFileExceptionErrorPage.ExceptionInfo(exception, responsePage));
				throw new RestartResponseException(LayoutFileExceptionErrorPage.class);
			}
			else
			{
				// restart wicket processing with DefaultErrorPageRequestTarget
				throw new RestartResponseWithTargetException(new DefaultErrorPageRequestTarget(exception));
			}
		}
	}

	/**
	 * The {@link IMarkupParserFactory} implementation used by
	 * {@link LayoutFileApplication}.
	 *
	 * @version $Revision: 13093 $ $Date: 2011-05-28 18:40:18 -0300 (Sat, 28 May 2011) $
	 * @since 6.0.0
	 */
	private class MarkupParserFactoryImpl extends MarkupParserFactory
	{
		public MarkupParser newMarkupParser(final MarkupResourceStream resource)
		{
			MarkupParser parser = super.newMarkupParser(resource);
			
			// remove RelativePathPrefixHandler from markupFilterChain in parser
			try
			{
				Field markupFilterChainField = parser.getClass().getDeclaredField("markupFilterChain");
				markupFilterChainField.setAccessible(true);
				IMarkupFilter markupFilter = (IMarkupFilter) markupFilterChainField.get(parser);
				if (markupFilter instanceof RelativePathPrefixHandler)
				{
					markupFilterChainField.set(parser, markupFilter.getParent());
				}
				else
				{
					while (markupFilter != null)
					{
						if (markupFilter.getParent() instanceof RelativePathPrefixHandler)
						{
							markupFilter.setParent(markupFilter.getParent().getParent());
							break;
						}
						markupFilter = markupFilter.getParent();
					}
				}
				
				if(resource instanceof LayoutFileMarkupResourceStream)
				{
					IPortalMode portalMode = ((LayoutFileMarkupResourceStream) resource).getPortalMode();
		
					Class<? extends AbstractLayoutFileMarkupFilter> markupFilterClass = portalMode.getPerspective().getMarkupFilterClass();
					Constructor<? extends AbstractLayoutFileMarkupFilter> markupFilterClassConstructor = null;
					markupFilterClassConstructor = markupFilterClass.getConstructor(new Class[]{boolean.class});
					
					parser.appendMarkupFilter(markupFilterClassConstructor.newInstance(new Object[]{getStripLayoutFileTags()}));
				}

				parser.appendMarkupFilter(new ELMarkupFilter(getStripLayoutFileTags()));
				
			}
			catch (Exception e)
			{
				throw new RuntimeException(e);
			}

			return parser;
		}
	}

	/**
	 * Class resolver for the layout file application.
	 *
	 * @version $Revision: 13093 $ $Date: 2011-05-28 18:40:18 -0300 (Sat, 28 May 2011) $
	 * @since 6.0.0
	 */
	private static class LayoutFileClassResolver implements IClassResolver
	{
		private IClassResolver wicketDefaultClassResolver = new DefaultClassResolver();
		
		public Class<?> resolveClass(String resolverClass) throws ClassNotFoundException
		{
			return ManagerFactory.getDeploymentManager().getClassLoader().loadClass(resolverClass);
		}

		public Iterator<URL> getResources(String resource)
		{
			return wicketDefaultClassResolver.getResources(resource);
		}
	}
	
	/**
	 * Resolves the components for {@link LayoutFileTag}.
	 *
	 * @version $Revision: 13093 $ $Date: 2011-05-28 18:40:18 -0300 (Sat, 28 May 2011) $
	 * @since 6.0.0
	 */
	@SuppressWarnings("serial")
	private class LayoutFileComponentResolver implements IComponentResolver
	{
		/**
		 * The {@link Component#setAuto(boolean)} method called by reflection
		 * by {@link #setAuto(Component, boolean)}.
		 * @since 6.0.0
		 */
		private final Method setAutoMethod;
		
		public LayoutFileComponentResolver()
		{
			try
			{
				setAutoMethod = Component.class.getDeclaredMethod("setAuto", Boolean.TYPE);
				setAutoMethod.setAccessible(true);
			}
			catch (Exception e)
			{
				throw new RuntimeException(e);
			}
		}
		
		/**
		 * Calls {@link Component#setAuto(boolean)}.
		 * @param component the component.
		 * @param value the new auto value.
		 * @since 6.0.0
		 */
		private void setAuto(Component component, boolean value)
		{
			try
			{
				setAutoMethod.invoke(component, value);
			}
			catch (Exception e)
			{
				throw new RuntimeException(e);
			}
		}
		
		public boolean resolve(MarkupContainer container, MarkupStream markupStream, ComponentTag tag)
		{
			if (tag instanceof LayoutFileTag)
			{
				// create the component
				LayoutFileTag layoutFileTag = (LayoutFileTag) tag;
				
				if (layoutFileTag.isIgnore())
				{
					markupStream.skipComponent();
					return true;
				}
				
				Component component = layoutFileTag.getComponentFactory().createComponent(container, markupStream, layoutFileTag);
				
				// add the component to the container
				container.autoAdd(component, markupStream);
				
				// disable auto status from the component
				setAuto(component, false);
				
				return true;
			}
			else
			{
				return false;
			}
		}
	}
	
	/**
	 * The page factory used by the layout file application. This implementation
	 * does not support {@link PageParameters} (they are ignored).
	 *
	 * @version $Revision: 13093 $ $Date: 2011-05-28 18:40:18 -0300 (Sat, 28 May 2011) $
	 * @since 6.0.0
	 */
	private static class LayoutFilePageFactory implements IPageFactory
	{
		public <C extends Page> Page newPage(Class<C> pageClass)
		{
			try
			{
				return pageClass.newInstance();
			}
			catch (Exception e)
			{
				throw new RuntimeException(e);
			}
		}

		public <C extends Page> Page newPage(Class<C> pageClass, PageParameters parameters)
		{
			return newPage(pageClass);
		}
	}
	
	@Override
	protected IRequestCycleProcessor newRequestCycleProcessor()
	{
		return new LayoutFileWebRequestCycleProcessor();
	}
	
	@Override
	protected void init()
	{
		super.init();
		
		getApplicationSettings().setClassResolver(new LayoutFileClassResolver());
		getSessionSettings().setPageFactory(new LayoutFilePageFactory());
		getRequestCycleSettings().setBufferResponse(true);
		getPageSettings().setAutomaticMultiWindowSupport(false);
		getPageSettings().setVersionPagesByDefault(false);
		getPageSettings().addComponentResolver(new LayoutFileComponentResolver());
		getMarkupSettings().setStripXmlDeclarationFromOutput(true);
		getMarkupSettings().setDefaultMarkupEncoding("UTF-8");
		getMarkupSettings().setMarkupParserFactory(new MarkupParserFactoryImpl());
		
		// setResourcePollFrequency according to layoutFilePollIntervalSecs portal configuration
		Integer layoutFilePollIntervalSecs = PortalContext.getLayoutFilePollIntervalSecs();
		if (layoutFilePollIntervalSecs != null)
		{
			if (layoutFilePollIntervalSecs > 0)
				getResourceSettings().setResourcePollFrequency(Duration.seconds(layoutFilePollIntervalSecs));
			else
				getResourceSettings().setResourcePollFrequency(null);
		}
	}
	
	@Override
	public String getConfigurationType()
	{
		return PortalConfiguration.getCurrentConfiguration().isDevelopmentMode() ?
				DEVELOPMENT : DEPLOYMENT;
	}

	@Override
	protected ISessionStore newSessionStore()
	{
		return LayoutFileSessionStoreFactory.newSessionStore(this);
	}
	
	public Class<? extends Page> getHomePage()
    {
        return LayoutFilePage.class;
    }

	/**
	 * Overridden to disable Wicket development mode warning output.
	 * @since 6.0.0
	 */
	@Override
	protected void outputDevelopmentModeWarning()
	{
	}
	
	/**
	 * Returns whether to remove layout file tags from the output.
	 * @return whether to remove layout file tags from the output.
	 * @since 6.0.0
	 */
	public boolean getStripLayoutFileTags()
	{
		return getMarkupSettings().getStripWicketTags();
	}
}